package com.ryan.ohos.extension.widget.expandablelist;


import ohos.agp.components.BaseItemProvider;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.miscservices.timeutility.Time;

import java.util.ArrayList;
import java.util.Collections;


class ExpandableListItemProvider extends BaseItemProvider {

    private ExpandableItemProvider mExpandableItemProvider;


    private ArrayList<GroupMetadata> mExpGroupMetadataList;


    private int mTotalExpChildrenCount;


    private int mMaxExpGroupCount = Integer.MAX_VALUE;

    private final DataSetObserver mDataSetObserver = new MyDataSetObserver();


    public ExpandableListItemProvider(ExpandableItemProvider expandableItemProvider) {
        mExpGroupMetadataList = new ArrayList<>();

        setExpandableListItemProvider(expandableItemProvider);

    }

    /**
     * setExpandableListItemProvider
     *
     * @param expandableItemProvider expandableItemProvider
     */
    public void setExpandableListItemProvider(ExpandableItemProvider expandableItemProvider) {
        if (mExpandableItemProvider != null) {
            mExpandableItemProvider.removeDataSetObserver(mDataSetObserver);
        }

        mExpandableItemProvider = expandableItemProvider;
        expandableItemProvider.addDataSetObserver(mDataSetObserver);
    }


    PositionMetadata getUnflattenedPos(final int flPos) {
        /* Keep locally since frequent use */
        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
        final int numExpGroups = egml.size();

        /* Binary search variables */
        int leftExpGroupIndex = 0;
        int rightExpGroupIndex = numExpGroups - 1;
        int midExpGroupIndex = 0;
        GroupMetadata midExpGm;

        if (numExpGroups == 0) {

            return PositionMetadata.obtain(flPos, ExpandableListContainerPosition.GROUP, flPos,
                    -1, null, 0);
        }


        while (leftExpGroupIndex <= rightExpGroupIndex) {
            midExpGroupIndex =
                    (rightExpGroupIndex - leftExpGroupIndex) / 2
                            + leftExpGroupIndex;
            midExpGm = egml.get(midExpGroupIndex);

            if (flPos > midExpGm.lastChildFlPos) {

                leftExpGroupIndex = midExpGroupIndex + 1;
            } else if (flPos < midExpGm.flPos) {

                rightExpGroupIndex = midExpGroupIndex - 1;
            } else if (flPos == midExpGm.flPos) {

                return PositionMetadata.obtain(flPos, ExpandableListContainerPosition.GROUP,
                        midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
            } else if (flPos <= midExpGm.lastChildFlPos) {

                final int childPos = flPos - (midExpGm.flPos + 1);
                return PositionMetadata.obtain(flPos, ExpandableListContainerPosition.CHILD,
                        midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
            }
        }


        int insertPosition = 0;

        int groupPos = 0;


        if (leftExpGroupIndex > midExpGroupIndex) {


            final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex - 1);

            insertPosition = leftExpGroupIndex;


            groupPos =
                    (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;
        } else if (rightExpGroupIndex < midExpGroupIndex) {


            final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);

            insertPosition = rightExpGroupIndex;


            groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
        } else {
            // TODO: clean exit
            throw new RuntimeException("Unknown state");
        }

        return PositionMetadata.obtain(flPos, ExpandableListContainerPosition.GROUP, groupPos, -1,
                null, insertPosition);
    }


    PositionMetadata getFlattenedPos(final ExpandableListContainerPosition pos) {
        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
        final int numExpGroups = egml.size();

        /* Binary search variables */
        int leftExpGroupIndex = 0;
        int rightExpGroupIndex = numExpGroups - 1;
        int midExpGroupIndex = 0;
        GroupMetadata midExpGm;

        if (numExpGroups == 0) {

            return PositionMetadata.obtain(pos.groupPos, pos.type,
                    pos.groupPos, pos.childPos, null, 0);
        }


        while (leftExpGroupIndex <= rightExpGroupIndex) {
            midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex) / 2 + leftExpGroupIndex;
            midExpGm = egml.get(midExpGroupIndex);

            if (pos.groupPos > midExpGm.gPos) {

                leftExpGroupIndex = midExpGroupIndex + 1;
            } else if (pos.groupPos < midExpGm.gPos) {

                rightExpGroupIndex = midExpGroupIndex - 1;
            } else if (pos.groupPos == midExpGm.gPos) {


                if (pos.type == ExpandableListContainerPosition.GROUP) {
                    return PositionMetadata.obtain(midExpGm.flPos, pos.type,
                            pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
                } else if (pos.type == ExpandableListContainerPosition.CHILD) {
                    return PositionMetadata.obtain(midExpGm.flPos + pos.childPos
                                    + 1, pos.type, pos.groupPos, pos.childPos,
                            midExpGm, midExpGroupIndex);
                } else {
                    return null;
                }
            }
        }


        if (pos.type != ExpandableListContainerPosition.GROUP) {
            /* If it isn't a group, return null */
            return null;
        }


        if (leftExpGroupIndex > midExpGroupIndex) {


            final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex - 1);
            final int flPos =
                    leftExpGm.lastChildFlPos
                            + (pos.groupPos - leftExpGm.gPos);

            return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
                    pos.childPos, null, leftExpGroupIndex);
        } else if (rightExpGroupIndex < midExpGroupIndex) {


            final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
            final int flPos =
                    rightExpGm.flPos
                            - (rightExpGm.gPos - pos.groupPos);
            return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
                    pos.childPos, null, rightExpGroupIndex);
        } else {
            return null;
        }
    }

    /**
     * areAllItemsEnabled
     *
     * @return boolean
     */
    public boolean areAllItemsEnabled() {
        return mExpandableItemProvider.areAllItemsEnabled();
    }

    /**
     * isEnabled
     *
     * @param flatListPos
     * @return boolean
     */
    public boolean isEnabled(int flatListPos) {
        final PositionMetadata metadata = getUnflattenedPos(flatListPos);
        final ExpandableListContainerPosition pos = metadata.position;

        boolean retValue;
        if (pos.type == ExpandableListContainerPosition.CHILD) {
            retValue = mExpandableItemProvider.isChildSelectable(pos.groupPos, pos.childPos);
        } else {
            // Groups are always selectable
            retValue = true;
        }

        metadata.recycle();

        return retValue;
    }

    /**
     * getCount
     *
     * @return int
     */
    public int getCount() {

        return mExpandableItemProvider.getGroupCount() + mTotalExpChildrenCount;
    }

    /**
     * getItem
     *
     * @param flatListPos flatListPos
     * @return int
     * @throws RuntimeException Flat list position is of unknown type
     */
    public Object getItem(int flatListPos) {
        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);

        Object retValue;
        if (posMetadata.position.type == ExpandableListContainerPosition.GROUP) {
            retValue = mExpandableItemProvider
                    .getGroupItem(posMetadata.position.groupPos);
        } else if (posMetadata.position.type == ExpandableListContainerPosition.CHILD) {
            retValue = mExpandableItemProvider.getChildItem(posMetadata.position.groupPos,
                    posMetadata.position.childPos);
        } else {
            // TODO: clean exit
            throw new RuntimeException("Flat list position is of unknown type");
        }

        posMetadata.recycle();

        return retValue;
    }

    /**
     * getItemId
     *
     * @param flatListPos
     * @return long
     * @throws RuntimeException Flat list position is of unknown type
     */
    public long getItemId(int flatListPos) {
        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
        final long groupId = mExpandableItemProvider.getGroupId(posMetadata.position.groupPos);

        long retValue;
        if (posMetadata.position.type == ExpandableListContainerPosition.GROUP) {
            retValue = mExpandableItemProvider.getCombinedGroupId(groupId);
        } else if (posMetadata.position.type == ExpandableListContainerPosition.CHILD) {
            final long childId = mExpandableItemProvider.getChildId(posMetadata.position.groupPos,
                    posMetadata.position.childPos);
            retValue = mExpandableItemProvider.getCombinedChildId(groupId, childId);
        } else {
            // TODO: clean exit
            throw new RuntimeException("Flat list position is of unknown type");
        }

        posMetadata.recycle();

        return retValue;
    }

    /**
     * getComponent
     *
     * @param flatListPos flatListPos
     * @param convertView convertView
     * @param parent      parent
     * @return Component
     */
    @Override
    public Component getComponent(int flatListPos, Component convertView, ComponentContainer parent) {
        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);

        Component retValue;
        if (posMetadata.position.type == ExpandableListContainerPosition.GROUP) {
            retValue = mExpandableItemProvider.getGroupComponent(posMetadata.position.groupPos,
                    posMetadata.isExpanded(), convertView, parent);
        } else if (posMetadata.position.type == ExpandableListContainerPosition.CHILD) {
            final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;

            retValue = mExpandableItemProvider.getChildComponent(posMetadata.position.groupPos,
                    posMetadata.position.childPos, isLastChild, convertView, parent);
        } else {
            // TODO: clean exit
            throw new RuntimeException("Flat list position is of unknown type");
        }

        posMetadata.recycle();
        return retValue;
    }

    /**
     * getItemComponentType
     *
     * @param flatListPos
     * @return int
     */
    @Override
    public int getItemComponentType(int flatListPos) {
        final PositionMetadata metadata = getUnflattenedPos(flatListPos);
        final ExpandableListContainerPosition pos = metadata.position;

        int retValue;
        if (mExpandableItemProvider instanceof TypeExpandableListContainer) {
            TypeExpandableListContainer adapter =
                    (TypeExpandableListContainer) mExpandableItemProvider;
            if (pos.type == ExpandableListContainerPosition.GROUP) {
                retValue = adapter.getGroupType(pos.groupPos);
            } else {
                final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
                retValue = adapter.getGroupTypeCount() + childType;
            }
        } else {
            if (pos.type == ExpandableListContainerPosition.GROUP) {
                retValue = 0;
            } else {
                retValue = 1;
            }
        }

        metadata.recycle();

        return retValue;
    }

    /**
     * getComponentTypeCount
     *
     * @return int
     */
    @Override
    public int getComponentTypeCount() {
        if (mExpandableItemProvider instanceof TypeExpandableListContainer) {
            TypeExpandableListContainer adapter =
                    (TypeExpandableListContainer) mExpandableItemProvider;
            return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
        } else {
            return 2;
        }
    }

    /**
     * hasStableIds
     *
     * @return boolean
     */
    public boolean hasStableIds() {
        return mExpandableItemProvider.hasStableIds();
    }


    @SuppressWarnings("unchecked")
    private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
                                             boolean syncGroupPositions) {
        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
        int egmlSize = egml.size();
        int curFlPos = 0;

        mTotalExpChildrenCount = 0;

        if (syncGroupPositions) {
            boolean positionsChanged = false;

            for (int i = egmlSize - 1; i >= 0; i--) {
                GroupMetadata curGm = egml.get(i);
                int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
                if (newGPos != curGm.gPos) {
                    if (newGPos == -1) {
                        egml.remove(i);
                        egmlSize--;
                    }

                    curGm.gPos = newGPos;
                    if (!positionsChanged) positionsChanged = true;
                }
            }

            if (positionsChanged) {
                Collections.sort(egml);
            }
        }

        int gChildrenCount;
        int lastGPos = 0;
        for (int i = 0; i < egmlSize; i++) {
            GroupMetadata curGm = egml.get(i);


            if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
                gChildrenCount = mExpandableItemProvider.getChildrenCount(curGm.gPos);
            } else {

                gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
            }

            mTotalExpChildrenCount += gChildrenCount;


            curFlPos += (curGm.gPos - lastGPos);
            lastGPos = curGm.gPos;

            curGm.flPos = curFlPos;
            curFlPos += gChildrenCount;
            curGm.lastChildFlPos = curFlPos;
        }
    }


    boolean collapseGroup(int groupPos) {
        ExpandableListContainerPosition elGroupPos = ExpandableListContainerPosition.obtain(
                ExpandableListContainerPosition.GROUP, groupPos, -1, -1);
        PositionMetadata pm = getFlattenedPos(elGroupPos);
        elGroupPos.recycle();
        if (pm == null) return false;

        boolean retValue = collapseGroup(pm);
        pm.recycle();
        return retValue;
    }

    boolean collapseGroup(PositionMetadata posMetadata) {

        if (posMetadata.groupMetadata == null) return false;

        mExpGroupMetadataList.remove(posMetadata.groupMetadata);

        refreshExpGroupMetadataList(false, false);

        notifyDataChanged();

        mExpandableItemProvider.onGroupCollapsed(posMetadata.groupMetadata.gPos);

        return true;
    }


    boolean expandGroup(int groupPos) {
        ExpandableListContainerPosition elGroupPos = ExpandableListContainerPosition.obtain(
                ExpandableListContainerPosition.GROUP, groupPos, -1, -1);
        PositionMetadata pm = getFlattenedPos(elGroupPos);
        elGroupPos.recycle();
        boolean retValue = expandGroup(pm);
        pm.recycle();
        return retValue;
    }

    boolean expandGroup(PositionMetadata posMetadata) {


        if (posMetadata.position.groupPos < 0) {
            // TODO clean exit
            throw new RuntimeException("Need group");
        }

        if (mMaxExpGroupCount == 0) return false;

        if (posMetadata.groupMetadata != null) return false;

        if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {

            GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);

            int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);

            collapseGroup(collapsedGm.gPos);

            if (posMetadata.groupInsertIndex > collapsedIndex) {
                posMetadata.groupInsertIndex--;
            }
        }

        GroupMetadata expandedGm = GroupMetadata.obtain(
                GroupMetadata.REFRESH,
                GroupMetadata.REFRESH,
                posMetadata.position.groupPos,
                mExpandableItemProvider.getGroupId(posMetadata.position.groupPos));

        mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);

        refreshExpGroupMetadataList(false, false);

        notifyDataChanged();

        mExpandableItemProvider.onGroupExpanded(expandedGm.gPos);

        return true;
    }

    /**
     * isGroupExpanded
     *
     * @param groupPosition
     * @return boolean
     */
    public boolean isGroupExpanded(int groupPosition) {
        GroupMetadata groupMetadata;
        for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
            groupMetadata = mExpGroupMetadataList.get(i);

            if (groupMetadata.gPos == groupPosition) {
                return true;
            }
        }

        return false;
    }

    /**
     * setMaxExpGroupCount
     *
     * @param maxExpGroupCount maxExpGroupCount
     */
    public void setMaxExpGroupCount(int maxExpGroupCount) {
        mMaxExpGroupCount = maxExpGroupCount;
    }

    ExpandableItemProvider getItemProvider() {
        return mExpandableItemProvider;
    }


    ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
        return mExpGroupMetadataList;
    }

    void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {

        if ((expandedGroupMetadataList == null) || (mExpandableItemProvider == null)) {
            return;
        }

        int numGroups = mExpandableItemProvider.getGroupCount();
        for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
            if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
                return;
            }
        }

        mExpGroupMetadataList = expandedGroupMetadataList;
        refreshExpGroupMetadataList(true, false);
    }

    /**
     * isEmpty
     *
     * @return boolean
     */
    public boolean isEmpty() {
        ExpandableItemProvider adapter = getItemProvider();
        return adapter != null ? adapter.isEmpty() : true;
    }

    int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
        int count = mExpandableItemProvider.getGroupCount();

        if (count == 0) {
            return -1;
        }

        if (groupIdToMatch == Long.MIN_VALUE) {
            return -1;
        }

        seedGroupPosition = Math.max(0, seedGroupPosition);
        seedGroupPosition = Math.min(count - 1, seedGroupPosition);

        long endTime = Time.getRealActiveTime() + 100;
        long rowId;

        int first = seedGroupPosition;

        int last = seedGroupPosition;

        boolean next = false;

        boolean hitFirst;

        boolean hitLast;


        ExpandableItemProvider itemProvider = getItemProvider();
        if (itemProvider == null) {
            return -1;
        }

        while (Time.getRealActiveTime() <= endTime) {
            rowId = itemProvider.getGroupId(seedGroupPosition);
            if (rowId == groupIdToMatch) {
                return seedGroupPosition;
            }

            hitLast = last == count - 1;
            hitFirst = first == 0;

            if (hitLast && hitFirst) {
                break;
            }

            if (hitFirst || (next && !hitLast)) {
                last++;
                seedGroupPosition = last;
                next = false;
                //} else if (hitLast || (!next && !hitFirst)) {
            } else if (elseBoolean(hitLast, next, hitFirst)) {
                first--;
                seedGroupPosition = first;
                next = true;
            }

        }

        return -1;
    }

    private boolean elseBoolean(boolean hitLast, boolean next, boolean hitFirst) {
        return hitLast || (!next && !hitFirst);
    }

    protected class MyDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            refreshExpGroupMetadataList(true, true);

            notifyDataChanged();
        }

        @Override
        public void onInvalidated() {
            refreshExpGroupMetadataList(true, true);
            notifyDataInvalidated();
        }
    }


    static class GroupMetadata implements Comparable<GroupMetadata> {
        final static int REFRESH = -1;


        int flPos;


        int lastChildFlPos;


        int gPos;

        long gId;

        private GroupMetadata() {
        }

        static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
            GroupMetadata gm = new GroupMetadata();
            gm.flPos = flPos;
            gm.lastChildFlPos = lastChildFlPos;
            gm.gPos = gPos;
            gm.gId = gId;
            return gm;
        }

        public int compareTo(GroupMetadata another) {
            if (another == null) {
                throw new IllegalArgumentException();
            }

            return gPos - another.gPos;
        }

        public int describeContents() {
            return 0;
        }
    }


    static public class PositionMetadata {

        private static final int MAX_POOL_SIZE = 5;
        private static ArrayList<PositionMetadata> sPool =
                new ArrayList<PositionMetadata>(MAX_POOL_SIZE);


        public ExpandableListContainerPosition position;


        public GroupMetadata groupMetadata;


        public int groupInsertIndex;

        private void resetState() {
            if (position != null) {
                position.recycle();
                position = null;
            }
            groupMetadata = null;
            groupInsertIndex = 0;
        }


        private PositionMetadata() {
        }

        static PositionMetadata obtain(int flatListPos, int type, int groupPos,
                                       int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
            PositionMetadata pm = getRecycledOrCreate();
            pm.position = ExpandableListContainerPosition.obtain(type, groupPos, childPos, flatListPos);
            pm.groupMetadata = groupMetadata;
            pm.groupInsertIndex = groupInsertIndex;
            return pm;
        }

        private static PositionMetadata getRecycledOrCreate() {
            PositionMetadata pm;
            synchronized (sPool) {
                if (sPool.size() > 0) {
                    pm = sPool.remove(0);
                } else {
                    return new PositionMetadata();
                }
            }
            pm.resetState();
            return pm;
        }

        /**
         * recycle
         */
        public void recycle() {
            resetState();
            synchronized (sPool) {
                if (sPool.size() < MAX_POOL_SIZE) {
                    sPool.add(this);
                }
            }
        }

        /**
         * isExpanded
         *
         * @return boolean
         */
        public boolean isExpanded() {
            return groupMetadata != null;
        }
    }
}

